#include "Database.h"

#include <QRegularExpression>
#include <QFile>
#include <QSqlQuery>
#include <QSqlError>
#include <QStringList>
#include <QList>
#include <QDebug>
#include <QException>
#include <QThread>
#include <cstdio>
#define q qDebug()
#define t(x) qDebug()<<#x":"<<(x)

using std::move;
using std::swap;

const QString Column::TypeName[3]= {"TEXT","REAL","INTEGER"};
const QVariant::Type Column::QVariantType[3]= {
    QVariant::Type::String,
    QVariant::Type::Double,
    QVariant::Type::Int
};

Column::Column(QString name, Column::Type type):name(name),type(type) {}

const QString &Column::getTypeName() {
    return TypeName[type];
}

Row::Row(const QSqlRecord &record) {
    load(record);
}

QVariant &Row::operator[](int i) {
    return values[i];
}

void Row::load(const QSqlRecord &record) {
    values.clear();
    for(int i=0; i<record.count(); i++)
        values.push_back(record.value(i));
}

int Row::size() {
    return values.size();
}

Table::Table(QString name, QString key) {
    this->name=name;
    if(!key.isEmpty()) {
        addColumn(Column(key));
        keyId=0;
    } else
		keyId=0;
//		keyId=-1;
}

void Table::setKey(int keyId) {
	this->keyId=keyId;
}

Column &Table::addColumn(Column &&column) {
    auto found=columnId.find(column.name);
    if(found!=columnId.end()) return columns[*found];
    columnId[column.name]=columns.size();
    columns.push_back(column);
    columnNames.push_back(column.name);
    for(Row &row:rows)
        row.values.push_back(QVariant(Column::QVariantType[column.type]));
    return columns.back();
}

Column &Table::getColumn(QString columnName) {
    return columns[columnId[columnName]];
}

Row &Table::addId(QString id) {
	assert(!columns.empty());
	auto found=rowId.find(id);
	if(found!=rowId.end()) return rows[*found];
	Row row;
	row.values.push_back(id);
	for(int i=1; i<columns.size(); i++)
        row.values.push_back(
            QVariant(Column::QVariantType[columns[i].type])
        );
    return addRow(move(row));
}

Row &Table::addRow(Row &&row) {
    assert(row.values.size()==columns.size());
    if(row.values.size()>keyId)
        rowId[row.values[keyId].toString()]=rows.size();
    rows.push_back(move(row));
    return rows.back();
}

Row &Table::getRow(QString id) {
    return rows[rowId[id]];
}

void Table::removeRow(QString id) {
    auto toRemove=rowId.find(id);
    if(toRemove==rowId.end()) return;
    auto toReplace=rowId.find(rows.back()[keyId].toString());
    swap(rows[*toRemove],rows[*toReplace]);
    *toReplace=*toRemove;
    rowId.erase(toRemove);
    rows.pop_back();
}

void Table::clearRows() {
    rows.clear();
    rowId.clear();
}

Table &Table::clone(QString newName) {
    Table *table=new Table();
    table->name=newName.isEmpty()?name:newName;
    table->keyId=keyId;
    table->columnId=columnId;
    table->columnNames=columnNames;
    table->columns=columns;
    return *table;
}

Database::Database() {
}

Table &Database::addTable(Table &&table) {
    auto found=tableId.find(table.name);
    if(found!=tableId.end()) return tables[*found];
    tables.append(table);
    tableId[table.name]=tables.size()-1;
    return tables.back();
}

Table &Database::getTable(QString tableName) {
    return tables[tableId[tableName]];
}

void Database::removeTable(QString tableName) {
    auto toRemove=tableId.find(tableName);
    if(toRemove==tableId.end()) return;
    auto toReplace=tableId.find(tables.back().name);
    swap(tables[*toRemove],tables[*toReplace]);
    *toReplace=*toRemove;
    tableId.erase(toRemove);
    tables.pop_back();
}

void Database::clearTables() {
    tables.clear();
    tableId.clear();
}

void Driver::setDatabase(QString name) {
    databaseName=name;
}

void Driver::loadDatabase(Database &database) {
    databaseName=database.name;
    for(Table &table:database.tables)
        loadTable(table);
}

void Driver::saveDatabase(Database &database) {
    databaseName=database.name;
    for(Table &table:database.tables)
        saveTable(table);
}

void Driver::loadTable(Table &table) {
    open();
    QSqlQuery query=
        connection.exec(
            QString("SELECT `%1` FROM `%2`")
            .arg(table.columnNames.join("`,`")).arg(table.name)
        );
//    assert(!query.lastError().isValid());
    table.clearRows();
    while(query.next())
        table.addRow(Row(query.record()));
    close();
}

void Driver::saveTable(Table &table) {
    open();
    if(!connection.record(table.name).isEmpty()) {
        connection.exec(
            QString("DROP TABLE `%1`")
            .arg(table.name)
        );
//        assert(!connection.lastError().isValid());
    }
    int nColumn=table.columns.size();
    QStringList columns;
    for(int i=0; i<nColumn; i++) {
        Column &column=table.columns[i];
        columns.append(
            QString("`%1` %2 %3")
            .arg(column.name)
            .arg(column.getTypeName())
            .arg(table.keyId==i?"NOT NULL PRIMARY KEY":"")
        );
    }
    connection.exec(
        QString("CREATE TABLE `%1` (%2)")
        .arg(table.name)
        .arg(columns.join(','))
    );
//    assert(!connection.lastError().isValid());
    QSqlQuery query=QSqlQuery(connection);
    query.prepare(
        QString("INSERT INTO `%1` (`%2`) VALUES (%3)")
        .arg(table.name)
        .arg(table.columnNames.join("`,`"))
        .arg(QStringList(QVector<QString>(nColumn,"?").toList()).join(","))
    );
    for(Row &row:table.rows) {
        for(int i=0; i<row.values.size(); i++)
            query.bindValue(i,row.values[i]);
        query.exec();
        if(query.lastError().isValid()) {
            QFile file("dbkit.dll.log");
            file.open(QFile::Append);
            file.write(query.lastError().text().toUtf8().data());
            file.write("\n");
            file.close();
            break;
        }
    }
    close();
}

QString currentThreadId() {
    typedef long long ll;
    return QString("%1").arg(ll(QThread::currentThreadId()));
}

void DriverSqlite::open() {
    connection = QSqlDatabase::addDatabase("QSQLITE",currentThreadId());
    connection.setDatabaseName(databaseName);
    connection.open();
    connection.exec("PRAGMA synchronous=OFF");
    connection.exec("PRAGMA journal_mode=OFF");
    connection.transaction();
}

void DriverSqlite::close() {
    connection.commit();
    QString connectionName=connection.connectionName();
    connection.close();
    connection=QSqlDatabase();
    QSqlDatabase::removeDatabase(connectionName);
}

void DriverSqlite::loadStructure(Database &database) {
    open();
    QString sqlGetTableInfo="PRAGMA table_info(`%1`)";
    QString typeName;
    const static QMap<QString,Column::Type> type= {
        {"int",Column::Type::Integer},
        {"integer",Column::Type::Integer},
        {"bigint",Column::Type::Integer},
        {"real",Column::Type::Real},
        {"double",Column::Type::Real},
        {"float",Column::Type::Real},
        {"text",Column::Type::Text},
        {"string",Column::Type::Text},
        {"varchar",Column::Type::Text}
    };
    for(QString tableName:connection.tables()) {
        Table table(tableName);
        QSqlQuery query=connection.exec(sqlGetTableInfo.arg(tableName));
        while(query.next()) {
            Column column(query.value(1).toString());
            typeName=query.value(2).toString().trimmed().toLower();
            column.type=type[typeName];
            if(query.value(5).toInt()==1)
                table.setKey(query.value(0).toInt());
            table.addColumn(move(column));
        }
        database.addTable(move(table));
    }
    close();
}
